using System;
using System.Text;
using System.Reflection;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization;
using Crainiate.Data.Persistence.Cache;
using Crainiate.Data.Persistence.Providers;

namespace Crainiate.Data.Persistence
{
    [Serializable]
	public class Query<T>: IQuery, ISerializable, IDeserializationCallback
    where T: class, IPersistable
	{
		//Property variables
        [NonSerialized] private Command _command;
		
        private Type _type;
        private Type _activateType;
		
        private IConditions _where;
        private IConditions _having;
        private IColumns _orders;
        private IColumns _groupBy;
        private IQueries _subQueries;
		private IResults _results;
        private IQuery _parent;
        private IColumns _joinFrom;
        private IColumns _joinTo;
        private bool _fill;

        private JoinType _joinType;

        //Working variables
        private SerializationInfo mInfo;
        int _cache;
        int _subCache;
        int _whereCache;
        int _orderByCache;
        int _groupByCache;
        int _havingCache;

        #region Interface

        //Constructors
		public Query()
		{
            _activateType = typeof(T);
            _type = typeof(T);
            _fill = true;
            _joinType = JoinType.Inner;

            //Sets mPersistType to the viewing type, not the activating type
            //and creates the internal objects
            Initialize();
		}

        public Query(bool fill)
        {
            _activateType = typeof(T);
            _type = typeof(T);
            _fill = fill;
            _joinType = JoinType.Inner;

            //Sets mPersistType to the viewing type, not the activating type
            //and create the internal objects
            Initialize();
        }

        protected Query(SerializationInfo info, StreamingContext context)
        {
            _activateType = typeof(T);
            _type = typeof(T);

            //Get the underlying persisted type, if relevant (eg a view)
            _type = Crainiate.Data.Persistence.Providers.Common.GetPersistType(_type);

            mInfo = info;
        }

		//Properties
        public virtual Command Command
        {
            get
            {
                return _command;
            }
            set
            {
                _command = value;
            }
        }

		public virtual Type Type
		{
			get
			{
				return _type;
			}
		}

        public virtual Type ActivateType
        {
            get
            {
                return _activateType;
            }
        }

        public virtual bool Fill
        {
            get
            {
                return _fill;
            }
            set
            {
                _fill = value;
                _cache = 0;
            }
        }

        public virtual IQueries SubQueries
        {
            get
            {
                return _subQueries;
            }
            set
            {
                _subQueries = value;
                _cache = 0;
            }
        }

		public virtual IConditions Where
		{
			get
			{
				return _where;
			}
			set
			{
				_where = value;
                _whereCache = 0;
			}
		}

        public virtual IColumns OrderBy
        {
            get
            {
                return _orders;
            }
            set
            {
                _orders = value;
                _orderByCache = 0;
            }
        }

        public virtual IColumns GroupBy
        {
            get
            {
                return _groupBy;
            }
            set
            {
                _groupBy = value;
                _groupByCache = 0;
            }
        }

        public virtual IConditions Having
        {
            get
            {
                return _having;
            }
            set
            {
                _having = value;
                _havingCache = 0;
            }
        }

		public virtual IResults Results
		{
			get
			{
				return _results;
			}
			set
			{
				_results = value;
			}
		}

        public virtual IQuery Parent
        {
            get
            {
                return _parent;
            }
            set
            {
                _parent = value;
                _cache = 0;
            }
        }

        public virtual IColumns JoinFrom
        {
            get
            {
                return _joinFrom;
            }
            set
            {
                _joinFrom = value;
            }
        }

        public virtual IColumns JoinTo
        {
            get
            {
                return _joinTo;
            }
            set
            {
                _joinTo = value;
            }
        }

        public virtual JoinType JoinType
        {
            get
            {
                return _joinType;
            }
            set
            {
                _joinType = value;
                _cache = 0;
            }
        }

		//Methods
        public virtual void Join(IQuery query)
        {
            query.Parent = this;

            //Attempt to join on the matching keys
            ReflectionCacheItem from = Component.Instance.ReflectionCache.Lookup(Type);
            ReflectionCacheItem to = Component.Instance.ReflectionCache.Lookup(query.Type);

            if ((from.HasPrimaryKey && to.HasPrimaryKey) || (from.HasCompoundKeys && to.HasCompoundKeys))
            {
                if (from.HasPrimaryKey)
                {
                    PropertyInfo fromprop = from.PrimaryKey;
                    PropertyInfo toprop = to.PrimaryKey;

                    if (fromprop.PropertyType != toprop.PropertyType) throw new QueryException("Primary key property types must be matching for " + fromprop.ToString() + " and " + toprop.ToString() + ".");

                    query.JoinFrom = new Columns(new PropertyInfo[] { fromprop });
                    query.JoinTo = new Columns(new PropertyInfo[] { toprop });
                }
                else
                {
                    PropertyInfo[] fromprops = from.CompoundKeys;
                    PropertyInfo[] toprops = to.CompoundKeys;

                    int i = 0;
                    foreach (PropertyInfo fromprop in fromprops)
                    {
                        PropertyInfo toprop = toprops[i];
                        if (fromprop.PropertyType != toprop.PropertyType) throw new QueryException("Compound key property types must be matching for " + fromprop.ToString() + " and " + toprop.ToString() + ".");
                        i++;
                    }

                    query.JoinFrom = new Columns(fromprops);
                    query.JoinTo = new Columns(toprops);
                }
            }
            else
            {
                throw new QueryException("Queries must have matching key types (primary or compound) to be joined automatically. Specify a From and To parameter to join these queries.");
            }


            SubQueries.Add(query);
        }

        public virtual void Join(IQuery query, string joinFrom, string joinTo)
        {
            //Attempt to join on on the field names
            PropertyInfo from = ActivateType.GetProperty(joinFrom);
            if (from == null) throw new QueryException("Property " + joinFrom + " was not found for Type " + ActivateType.ToString() + ".");

            PropertyInfo to = query.ActivateType.GetProperty(joinTo);
            if (to == null) throw new QueryException("Property " + joinTo + " was not found for Type " + ActivateType.ToString() + ".");

            query.Parent = this;
            query.JoinFrom = new Columns(new PropertyInfo[] { from });
            query.JoinTo = new Columns(new PropertyInfo[] { to });
            SubQueries.Add(query);
        }

        public virtual void Join(IQuery query, string[] joinFrom, string[] joinTo)
        {
            query.Parent = this;
            query.JoinFrom = new Columns(ActivateType, joinFrom);
            query.JoinTo = new Columns(query.ActivateType, joinTo);
            SubQueries.Add(query);
        }

        public virtual void Join(IQuery query, IColumn joinFrom, IColumn joinTo)
        {
            //Validate
            if (joinFrom.PropertyInfo.ReflectedType != ActivateType) throw new QueryException("The joinFrom property is not a property of Type " + ActivateType.ToString() + ".");
            if (joinTo.PropertyInfo.ReflectedType != query.ActivateType) throw new QueryException("The joinTo property is not a property of Type " + query.ActivateType.ToString() + ".");

            query.Parent = this;
            query.JoinFrom = new Columns();
            query.JoinFrom.Add(joinFrom);
            query.JoinTo = new Columns();
            query.JoinTo.Add(joinTo);
            
            SubQueries.Add(query);
        }

        public virtual void Join(IQuery query, IColumns joinFrom, IColumns joinTo)
        {
            //Validate
            foreach (IColumn field in joinFrom)
            {
                if (field.PropertyInfo.ReflectedType != ActivateType) throw new QueryException("A joinFrom property is not a property of Type " + ActivateType.ToString() + ".");
            }
            foreach (IColumn field in joinTo)
            {
                if (field.PropertyInfo.ReflectedType != query.ActivateType) throw new QueryException("A joinTo property is not a property of Type " + query.ActivateType.ToString() + ".");
            }

            query.Parent = this;
            query.JoinFrom = joinFrom;
            query.JoinTo = joinTo;
            SubQueries.Add(query);
        }

        public virtual void Join(IQuery query, PropertyInfo joinFrom, PropertyInfo joinTo)
        {
            Join(query, new Column(joinFrom), new Column(joinTo));
        }

        public virtual void Join(IQuery query, PropertyInfo[] joinFrom, PropertyInfo[] joinTo)
        {
            Join(query, new Columns(joinFrom), new Columns(joinTo));
        }

		public virtual void Execute()
		{
            if (Command == null) throw new QueryException("The query could not be executed. Set the Command property before executing a query.");
            Command.Execute(this);
		}

        #endregion

        #region Overrides

        //Do not append a parent hash code as this will create a circular reference
        public sealed override int GetHashCode()
        {
            int subCache = 0;
            int whereCache = 0;
            int orderByCache = 0;
            int groupByCache = 0;
            int havingCache = 0;

            if (SubQueries != null) subCache = SubQueries.GetHashCode(); 
            if (Where != null) whereCache = Where.GetHashCode();
            if (OrderBy != null) orderByCache = OrderBy.GetHashCode();
            if (GroupBy != null) groupByCache = GroupBy.GetHashCode();
            if (Having != null) havingCache = Having.GetHashCode();

            if (_cache != 0 && subCache == _subCache && whereCache == _whereCache && orderByCache == _orderByCache && groupByCache == _groupByCache && havingCache == _havingCache) return _cache;

            StringBuilder builder = new StringBuilder();
            
            //Append this types full name
            builder.Append(Type.FullName);

            //Append the join type
            builder.Append(JoinType.ToString());

            //Append the conditions if any
            if (Where != null && Where.Count > 0) builder.Append(Where.GetHashCode());

            //Append the sort hash if any
            if (OrderBy != null && OrderBy.Count > 0) builder.Append(OrderBy.GetHashCode());

            //Append the group by hash if any
            if (GroupBy != null && GroupBy.Count > 0) builder.Append(GroupBy.GetHashCode());

            //Append the conditions if any
            if (Having != null && Having.Count > 0) builder.Append(Having.GetHashCode());

            //Add an extra value if filling the recordset
            if (Fill) builder.Append(1);

            //Add sub query hashes
            if (SubQueries != null && SubQueries.Count > 0)
            {
                foreach (IQuery subquery in SubQueries)
                {
                    builder.Append(subquery.GetHashCode());
                }
            }

            _cache = builder.ToString().GetHashCode();
            _subCache = subCache;
            _whereCache = whereCache;
            _orderByCache = orderByCache;
            _groupByCache = groupByCache;
            _havingCache = havingCache;

            return _cache;
        }

        #endregion

        #region Implementation

        private void Initialize()
        {
            //Get the underlying persisted type, if relevant (eg a view)
            _type = Crainiate.Data.Persistence.Providers.Common.GetPersistType(_type);

            //Create child objects
            _subQueries = new Queries();
            _where = new Conditions();
            _orders = new Columns();
            if (_fill) Results = new Results<T>();

            //Reset cached hash values
            _cache = 0;
            _subCache = 0;
            _whereCache = 0;
            _orderByCache = 0;
            _groupByCache = 0;
            _havingCache = 0;
        }

        //Methods
        public virtual void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
        {
            info.AddValue("Fill", Fill);
            info.AddValue("SubQueries", SubQueries);
            info.AddValue("Where", Where);
            info.AddValue("OrderBy", OrderBy);
            info.AddValue("GroupBy", GroupBy);
            info.AddValue("Having", Having);
            info.AddValue("Results", Results);
            info.AddValue("Parent", Parent);
            info.AddValue("JoinFrom", JoinFrom);
            info.AddValue("JoinTo", JoinTo);
        }

        public virtual void OnDeserialization(object sender)
        {
            _fill = mInfo.GetBoolean("Fill");
            _subQueries = mInfo.GetValue("SubQueries", typeof(IQueries)) as IQueries;
            _where = mInfo.GetValue("Where", typeof(IConditions)) as IConditions;
            _orders = mInfo.GetValue("OrderBy", typeof(IColumns)) as IColumns;
            _groupBy = mInfo.GetValue("GroupBy", typeof(IColumns)) as IColumns;
            _having = mInfo.GetValue("Having", typeof(IConditions)) as IConditions;
            _results = mInfo.GetValue("Results", typeof(IResults)) as IResults;
            _parent = mInfo.GetValue("Parent", typeof(IQuery)) as IQuery;
            _joinFrom = mInfo.GetValue("JoinFrom", typeof(IColumns)) as IColumns;
            _joinTo = mInfo.GetValue("JoinTo", typeof(IColumns)) as IColumns;

            mInfo = null;
        }

        #endregion
    }
}
